Coverage Report

Created: 2026-02-05 09:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\scloud-dns\scloud-dns\src\utils\logging.rs
Line
Count
Source
1
use crate::config::{LogFormat, LogLevel, LoggingConfig};
2
use std::fs::{self, File, OpenOptions};
3
use std::io::{self, Write};
4
use std::path::{Path, PathBuf};
5
use std::sync::{Mutex, OnceLock};
6
use std::time::SystemTime;
7
use crate::utils::time::{format_system_time, now_epoch_ms};
8
9
struct Logger {
10
    cfg: LoggingConfig,
11
    file: File,
12
}
13
14
static LOGGER: OnceLock<Mutex<Logger>> = OnceLock::new();
15
16
/// Initialize global logger.</br>
17
/// Call once at startup.
18
0
pub fn init(cfg: LoggingConfig) -> io::Result<()> {
19
0
    let path = Path::new(&cfg.file);
20
21
0
    if let Some(parent) = path.parent() {
22
0
        fs::create_dir_all(parent)?;
23
0
    }
24
25
0
    let file = OpenOptions::new()
26
0
        .create(true)
27
0
        .append(true)
28
0
        .open(path)?;
29
30
0
    let logger = Logger { cfg, file };
31
0
    let _ = LOGGER.set(Mutex::new(logger));
32
0
    Ok(())
33
0
}
34
35
36
/// Write one log line (internal).</br>
37
/// Safe to call from any thread.
38
0
pub fn log(level: LogLevel, target: &str, msg: &str) {
39
0
    let Some(lock) = LOGGER.get() else {
40
0
        return;
41
    };
42
43
0
    let mut g = match lock.lock() {
44
0
        Ok(g) => g,
45
0
        Err(poisoned) => poisoned.into_inner(),
46
    };
47
48
0
    if g.cfg.live_print == true {
49
0
        let now = SystemTime::now();
50
0
        println!("[{}][{:?}][{}] - {}", format_system_time(now), level, target, msg);
51
0
    }
52
53
0
    if level < g.cfg.level {
54
0
        return;
55
0
    }
56
57
0
    if g.cfg.rotate {
58
0
        let max_bytes = g.cfg.max_size_mb.saturating_mul(1024 * 1024);
59
0
        if max_bytes > 0 {
60
0
            if let Ok(meta) = g.file.metadata() {
61
0
                if meta.len() >= max_bytes {
62
0
                    let _ = rotate_file(&mut *g);
63
0
                }
64
0
            }
65
0
        }
66
0
    }
67
68
0
    let line = match g.cfg.format {
69
0
        LogFormat::JSON => format_json_line(level, target, msg),
70
0
        LogFormat::TEXT => format_text_line(level, target, msg),
71
    };
72
73
0
    let _ = g.file.write_all(line.as_bytes());
74
0
    let _ = g.file.write_all(b"\n");
75
0
    let _ = g.file.flush();
76
0
}
77
78
0
fn rotate_file(logger: &mut Logger) -> io::Result<()> {
79
0
    let path = Path::new(&logger.cfg.file);
80
81
0
    let epoch_ms = now_epoch_ms();
82
0
    let rotated = rotated_name(path, epoch_ms);
83
84
0
    let _ = logger.file.flush();
85
86
0
    let _ = fs::rename(path, &rotated);
87
88
0
    logger.file = OpenOptions::new()
89
0
        .create(true)
90
0
        .append(true)
91
0
        .open(path)?;
92
93
0
    Ok(())
94
0
}
95
96
0
fn rotated_name(path: &Path, epoch_ms: u128) -> PathBuf {
97
0
    let mut p = path.as_os_str().to_owned();
98
0
    let suffix = format!(".{}", epoch_ms);
99
0
    p.push(suffix);
100
0
    PathBuf::from(p)
101
0
}
102
103
0
fn format_text_line(level: LogLevel, target: &str, msg: &str) -> String {
104
0
    format!("{} {} {} {}", now_epoch_ms(), level.as_str(), target, msg)
105
0
}
106
107
0
fn format_json_line(level: LogLevel, target: &str, msg: &str) -> String {
108
0
    let msg_esc = json_escape(msg);
109
0
    let target_esc = json_escape(target);
110
0
    format!(
111
        r#"{{"ts":{},"level":"{}","target":"{}","msg":"{}"}}"#,
112
0
        now_epoch_ms(),
113
0
        level.as_str(),
114
        target_esc,
115
        msg_esc
116
    )
117
0
}
118
119
0
fn json_escape(s: &str) -> String {
120
0
    let mut out = String::with_capacity(s.len() + 8);
121
0
    for c in s.chars() {
122
0
        match c {
123
0
            '"' => out.push_str("\\\""),
124
0
            '\\' => out.push_str("\\\\"),
125
0
            '\n' => out.push_str("\\n"),
126
0
            '\r' => out.push_str("\\r"),
127
0
            '\t' => out.push_str("\\t"),
128
0
            c if c.is_control() => {
129
                use std::fmt::Write as _;
130
0
                let _ = write!(out, "\\u{:04x}", c as u32);
131
            }
132
0
            _ => out.push(c),
133
        }
134
    }
135
0
    out
136
0
}
137
138
#[macro_export]
139
macro_rules! log_error {
140
    ($target:expr, $($arg:tt)*) => {
141
        $crate::utils::logging::log($crate::utils::logging::LogLevel::Error, $target, &format!($($arg)*))
142
    };
143
}
144
#[macro_export]
145
macro_rules! log_warn {
146
    ($target:expr, $($arg:tt)*) => {
147
        $crate::utils::logging::log($crate::utils::logging::LogLevel::Warn, $target, &format!($($arg)*))
148
    };
149
}
150
#[macro_export]
151
macro_rules! log_info {
152
    ($target:expr, $($arg:tt)*) => {
153
        $crate::utils::logging::log($crate::utils::logging::LogLevel::Info, $target, &format!($($arg)*))
154
    };
155
}
156
#[macro_export]
157
macro_rules! log_debug {
158
    ($target:expr, $($arg:tt)*) => {
159
        $crate::utils::logging::log($crate::utils::logging::LogLevel::Debug, $target, &format!($($arg)*))
160
    };
161
}
162
#[macro_export]
163
macro_rules! log_trace {
164
    ($target:expr, $($arg:tt)*) => {
165
        $crate::utils::logging::log($crate::utils::logging::LogLevel::Trace, $target, &format!($($arg)*))
166
    };
167
}